package com.dmx.net.ftp.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.SocketException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.dmx.net.ConnectionException;
import com.dmx.net.NetworkProtocolClient;
import com.dmx.net.SocketFactoryTimeOut;

public class FtpClient implements NetworkProtocolClient {

	protected FTPClient ftpClient = new FTPClient();

	public static final String protocol = "FTP";

	private boolean isLogined = false;

	private static Logger log = LoggerFactory.getLogger(FtpClient.class);

	private String remoteHost;

	private int port = -1;

	private String userName = "";

	private String passwd = "";

	private String defaultEncoding = null;

	Pattern pRipDir = Pattern.compile("([^/]+)[/]{0,1}$");

	//final static int defaultTimeout = 1000 * 60 * 3; // 3 minutes to timeout
	final static int defaultTimeout = 1000 * 20;

	private int timeout = defaultTimeout;
	
	private int retry_num = 1;
	public static final int retryCount = 1;

	public FTPFile[] listFiles() throws IOException {
		return this.ftpClient.listFiles();
	}

	public FTPFile[] listFiles(String pathName) throws IOException {
		return this.ftpClient.listFiles(pathName);
	}

	public ConnectionException genConnectionException(Exception e) {
		String ftpReply = ftpClient.getReplyString();

		return new ConnectionException(e, ftpReply);
	}

	public boolean safeLogin() throws ConnectionException {

		try {
			this.close();
		} catch (Exception e) {

		}

		try {

			this.ftpClient.setDataTimeout(this.timeout);
			this.ftpClient.setDefaultTimeout(this.timeout);
			this.ftpClient.setReaderThread(false);

			if (log.isDebugEnabled()) {
				log.debug(
						"both dataTimeout and defaultTimeout are setted to:{}",
						this.timeout);
			}

			doLogin();

			// this.ftpClient.setSoTimeout(timeout);

		} catch (ConnectionException e) {
			log.error(e.getMessage());

			try {
				ftpClient.disconnect();
			} catch (IOException eDisconnect) {
				log.error(eDisconnect.getMessage());
				genConnectionException(eDisconnect);
			}

			doLogin();
		}
		return isLogined;
	}

	public boolean removeDir(String removeDir) throws ConnectionException,
			IOException {
		if (removeDir.equals(".") || removeDir.equals(".."))
			return true;

		if (removeDir.endsWith("/") == false)
			removeDir += "/";

		log.debug("begin to remove [{}]", removeDir);
		if (!cdToRemoteDir(removeDir))
			return false;

		try {
			FTPFile[] ftpFiles = ftpClient.listFiles(".");
			if (ftpFiles == null) {
				return false;
			}

			for (FTPFile f : ftpFiles) {
				if (f.isFile()) {
					if (!ftpClient.deleteFile(f.getName()))
						break;
				} else if (f.isDirectory()) {
					if (!removeDir(f.getName()))
						break;
				}
			}

		} catch (IOException e) {
			log.error("Failed to remove folder" + removeDir, e);
		}

		if (!ftpClient.changeToParentDirectory())
			return false;

		return ftpClient.removeDirectory(removeDir);
	}

	public void doLogin() throws ConnectionException {
		try {
			ftpClient.setSocketFactory(new SocketFactoryTimeOut(defaultTimeout));
			ftpClient.setDefaultTimeout(defaultTimeout);
			log.info("connecting to ftp://" + remoteHost + ":" + port);
			ftpClient.connect(remoteHost, port);
			log.info("try to log with username:password - " + userName + ":"
					+ passwd);
			isLogined = ftpClient.login(userName, passwd);
			log.debug(ftpClient.getReplyString());

			if (isLogined == true) {
				FTPClientConfig conf = new FTPClientConfig(
						FTPClientConfig.SYST_UNIX);
				ftpClient.configure(conf);
				ftpClient.setFileType(FTP.IMAGE_FILE_TYPE);
				int reply = ftpClient.sendCommand("OPTS UTF8 ON");// try to
				// try to alter to
				// UTF-8

				log.debug("alter to utf-8 encoding - reply:", ftpClient
						.getReplyString());

				if (reply == 200) { // UTF8 Command
					ftpClient.setControlEncoding("UTF-8");
				} else {
					ftpClient.setControlEncoding(getDefaultEncoding());
				}

				log
						.debug("current encoding:{}", ftpClient
								.getControlEncoding());
			}

		} catch (SocketException e) {
			log.error("Failed to connect/login", e);
			retry_num++;
			if(retry_num <= retryCount){
				doLogin();
			}
			else{
				throw genConnectionException(e);	
			}

		} catch (IOException e) {
			log.error(e.getMessage());
			retry_num++;
			if(retry_num <= retryCount){
				doLogin();
			}
			else{
				throw genConnectionException(e);	
			}
		}
	}

	public boolean deleteResource(String destName) throws ConnectionException {
		try {
			return ftpClient.deleteFile(destName);
		} catch (IOException e) {
			log.error(e.getMessage());
			throw genConnectionException(e);
		}
	}

	public boolean putFile(File localFile, String remoteFile, boolean isResumed)
			throws ConnectionException, IOException {

		if (log.isDebugEnabled()) {
			log.debug("begin to putfile:{} to {}", localFile.getAbsolutePath(),
					remoteFile);
		}

		long bytesTransfered = getResourceSize(remoteFile);

		if (!localFile.isFile() || !localFile.exists()) {
			throw new FileNotFoundException("File not exsited: ["
					+ localFile.getAbsolutePath() + "]");
		}

		FileInputStream fiStream = null;

		try {
			fiStream = new FileInputStream(localFile);
			if (true == isResumed) {
				if (bytesTransfered > localFile.length()) {
					throw new IOException(
							"Remote File existed, and bigger then the local src File");
				} else if (localFile.length() == bytesTransfered) {

					// If it's a resumed upload, we do nothing
					return true;
				}

				fiStream.skip(bytesTransfered);
			} else {
				this.ftpClient.deleteFile(remoteFile);
			}

			return this.ftpClient.appendFile(remoteFile, fiStream);

		} finally {
			if (fiStream != null) {
				try {
					fiStream.close();
				} catch (Throwable t) {

				}
			}
		}

	}

	public boolean getFile(File localFile, String remoteFile, boolean isResumed)
			throws ConnectionException, IOException,
			RemoteFtpFileNotExistsException {
		FileOutputStream foStream = null;

		long offSet = 0;

		if (log.isDebugEnabled()) {
			log.debug("begin to getFile:{} to {}", remoteFile, localFile
					.getAbsolutePath());
		}

		this.testRemoteFileExists(remoteFile);

		long streamSize = getResourceSize(remoteFile);

		if (0 == streamSize) {
			String warn = "File[" + remoteFile
					+ "] size is ZERO, will NOT retrive it ... ";
			log.warn(warn);
			throw new IOException(warn);
		}

		if (true == isResumed) {
			foStream = new FileOutputStream(localFile, true);
			offSet = localFile.length();
		} else {
			foStream = new FileOutputStream(localFile);
			offSet = 0;
		}

		try {
			if (offSet > 0) {
				ftpClient.setRestartOffset(offSet);
			}

			return ftpClient.retrieveFile(remoteFile, foStream);

		} finally {
			if (foStream != null) {
				try {
					foStream.close();
				} catch (Throwable t) {

				}
			}
		}

	}

	public long getResourceSize(String destName) throws ConnectionException {
		try {
			FTPFile[] fArray = ftpClient.listFiles();
			if (fArray == null)
				return 0;

			for (FTPFile f : fArray) {
				if (f.isFile() && f.getName().equals(destName)) {
					return f.getSize();
				}
			}

			for (FTPFile f : fArray) {
				if (f.isFile() && f.getName().equalsIgnoreCase(destName)) {
					return f.getSize();
				}
			}

		} catch (IOException e) {
			log.error(e.getMessage());
			throw genConnectionException(e);
		}
		return 0;
	}

	public void testRemoteFileExists(String remoteFileName)
			throws ConnectionException, RemoteFtpFileNotExistsException {
		log.info("remoteFileName in testRemoteFileExists is:"+remoteFileName);
		if (remoteFileName.endsWith("/") == true) {
			log.warn("please input a file, not a directory: [{}]",
					remoteFileName);
			throw new RemoteFtpFileNotExistsException(remoteFileName, this
					.getCurrentHostFullUrl());

		}

		try {

			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				log.error(e.getMessage());
			}
			FTPFile[] fArray = ftpClient.listFiles(remoteFileName);
			if (fArray != null && fArray.length == 1) {
				FTPFile f = fArray[0];
				if (f.isFile()) {
					log.info("remote file exists: [{}]", remoteFileName);
					return;
				}
			}
		} catch (IOException e) {
			log.error(e.getMessage());
			throw genConnectionException(e);
		} catch(Exception e){
			log.error("ftpClient.testRemoteFileExists Exception :",e);
		}

		log.warn("remote file *NOT* exists: [{}]", remoteFileName);
		throw new RemoteFtpFileNotExistsException(remoteFileName, this
				.getCurrentHostFullUrl());

	}

	public boolean testDirExists(String dir) throws ConnectionException {
		String dirName = dir;
		Matcher m = pRipDir.matcher(dir);
		if (m.find()) {
			dirName = m.group(1);
		}

		if (dir.endsWith("/"))
			dir = dir.substring(0, dir.length() - 1);

		try {
			FTPFile[] ftpFiles = ftpClient.listFiles(dir);

			if (ftpFiles == null)
				return false;

			for (FTPFile f : ftpFiles) {
				if (f.isDirectory())
					return true;
			}
		} catch (IOException e) {
			throw genConnectionException(e);
		} catch (Exception e) {
			log.error(e.getMessage());
		}

		return false;
	}

	public boolean createRemoteFolder(String dir) throws ConnectionException {
		String oldDir = null;
		boolean ret = false;

		log.debug("begin to createRemoteFolder: {}", dir);

		try {
			String dirs[] = dir.split("/");

			if (dirs == null) {
				return false;
			}

			oldDir = ftpClient.printWorkingDirectory();
			for (String dirTo : dirs) {
				if (dirTo.length() > 0) {// avoid ""
					if (testDirExists(dirTo) == false) {
						ftpClient.makeDirectory(dirTo);// if the folder already
						// exists, it will cause
						// execution of mkdir fails
					}
					if (!ftpClient.changeWorkingDirectory(dirTo))
						return false;
				}
			}
			ret = true;
			log.debug("Succssed in executing createRemoteFolder: {}", dir);
		} catch (IOException e) {
			log.debug("Failed in executing createRemoteFolder: {}", dir);
			throw genConnectionException(e);
		} finally {
			try {
				ftpClient.changeWorkingDirectory(oldDir);
			} catch (IOException e) {
				throw genConnectionException(e);
			}
		}
		return ret;
	}

	public boolean cdToRemoteDir(String dir) throws ConnectionException {
		try {
			log.debug("begin to cdToRemoteDir: {}", dir);
			boolean ret = ftpClient.changeWorkingDirectory(dir);
			if (ret == true) {
				log.debug("Succssed in executing cdToRemoteDir: {}", dir);
			} else {
				log.debug("Failed in executing cdToRemoteDir: {}", dir);
			}

			return ret;
		} catch (IOException e) {
			log.debug("Failed in executing cdToRemoteDir: {}", dir);
			log.error(e.getMessage());
			throw genConnectionException(e);
		}

	}

	public String getPasswd() {
		return passwd;
	}

	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public String getRemoteHost() {
		return remoteHost;
	}

	public void setRemoteHost(String remoteHost) {
		this.remoteHost = remoteHost;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public boolean isLogined() {
		return isLogined;
	}

	public void close() {
		if (ftpClient.isConnected()) {
			try {
				ftpClient.logout();
				ftpClient.disconnect();
			} catch (Exception e) {
				log.error(e.getMessage());
			}
		}
	}

	protected void finalize() {
		try {
			this.close();
		} catch (Throwable t) {

		} finally {
			try {
				super.finalize();
			} catch (Throwable t1) {
			}
		}
	}

	public String getDefaultEncoding() {
		if (defaultEncoding == null)
			defaultEncoding = System.getProperty("file.encoding");
		if (defaultEncoding != null)
			return defaultEncoding;
		else
			return "UTF-8";
	}

	public void setDefaultEncoding(String defaultEncoding) {
		this.defaultEncoding = defaultEncoding;
	}

	public boolean navigateToFolder(String folderUrlString)
			throws ConnectionException, IOException {
		boolean ret = cdToRemoteDir(folderUrlString);

		if (ret == false) {
			log.error("failed to navigateToFolder:[" + folderUrlString
					+ "] on FTP Server " + this.getRemoteHost() + ":"
					+ this.getPort());
			log
					.error("Pls check the ftp configuration, make sure the option that 'Lock user in home dir' is checked");
		}
		return ret;
	}

	public String getProtocol() {
		return protocol;
	}

	public String getCurrentWorkingDirectory() throws IOException {
		return ftpClient.printWorkingDirectory();
	}

	public String getCurrentWorkingDirectoryFullUrl() throws IOException {
		StringBuffer ret = new StringBuffer();

		ret.append("ftp://").append(this.getUserName()).append(":").append(
				this.getPasswd()).append("@").append(this.getRemoteHost())
				.append(":").append(this.getPort()).append(
						getCurrentWorkingDirectory());

		String retStr = ret.toString();
		if (retStr.endsWith("/") == false) {
			retStr += "/";
		}
		return retStr;
	}

	public String getCurrentHostFullUrl() {
		StringBuffer ret = new StringBuffer();
		ret.append("ftp://").append(this.getUserName()).append(":").append(
				this.getPasswd()).append("@").append(this.getRemoteHost())
				.append(":").append(this.getPort());

		String retStr = ret.toString();

		if (retStr.endsWith("/") == false) {
			retStr += "/";
		}
		return retStr;
	}

	public int getTimeout() {
		return timeout;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

}
